﻿using Bookify.Application.Abstractions.Clock;
using Bookify.Application.Abstractions.Data;
using Bookify.Domain.Abstractions;
using Dapper;
using MediatR;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Quartz;
using System.Data;

namespace Bookify.Infrastructure.Outbox
{
    [DisallowConcurrentExecution]
    internal sealed class ProcessOutboxMessagesJob : IJob
    {
        private static readonly JsonSerializerSettings JsonSerializerSettings = new()
        {
            TypeNameHandling = TypeNameHandling.All
        };

        private readonly ILogger<ProcessOutboxMessagesJob> _logger;
        private readonly ISqlConnectionFactory _sqlConnectionFactory;
        private readonly IPublisher _publisher;
        private readonly OutboxOptions _outboxOptions;
        private readonly IDateTimeProvider _dateTimeProvider;

        public ProcessOutboxMessagesJob(
            ILogger<ProcessOutboxMessagesJob> logger, 
            ISqlConnectionFactory sqlConnectionFactory, 
            IPublisher publisher, 
            IOptions<OutboxOptions> outboxOptions, 
            IDateTimeProvider dateTimeProvider)
        {
            _logger = logger;
            _sqlConnectionFactory = sqlConnectionFactory;
            _publisher = publisher;
            _outboxOptions = outboxOptions.Value;
            _dateTimeProvider = dateTimeProvider;
        }

        public async Task Execute(IJobExecutionContext context)
        {
            _logger.LogInformation("开始处理Outbox消息");
            using IDbConnection connection = _sqlConnectionFactory.CreateConnection();
            using IDbTransaction transaction = connection.BeginTransaction();

            IReadOnlyList<OutboxMessageResponse> outboxMessages = await GetOutboxMessages(connection, transaction);

            foreach (OutboxMessageResponse outboxMessage in outboxMessages)
            {
                Exception? exception = null;

                try
                {
                    IDomainEvent domainEvent = JsonConvert.DeserializeObject<IDomainEvent>(outboxMessage.Content, JsonSerializerSettings)!;

                    await _publisher.Publish(domainEvent, context.CancellationToken);
                }
                catch (Exception caughtException)
                {
                    _logger.LogError(
                        caughtException,
                        "处理Outbox消息异常{MessgeId}",
                        outboxMessage.Id);

                    exception = caughtException;
                }

                await UpdateOutboxMessageAsync(connection, transaction, outboxMessage, exception);
            }

            transaction.Commit();
            _logger.LogInformation("处理Outbox消息完成");

        }

        private async Task<IReadOnlyList<OutboxMessageResponse>> GetOutboxMessages(IDbConnection connection, IDbTransaction transaction)
        {
            string sql = $"""
                      SELECT id, content
                      FROM outbox_messages
                      WHERE processed_on_utc IS NULL
                      ORDER BY occurred_on_utc
                      LIMIT {_outboxOptions.BatchSize}
                      FOR UPDATE
                      """;

            IEnumerable<OutboxMessageResponse> outboxMessages = await connection.QueryAsync<OutboxMessageResponse>(
                sql,
                transaction: transaction);

            return outboxMessages.ToList();
        }

        private Task UpdateOutboxMessageAsync(
            IDbConnection connection,
            IDbTransaction transaction,
            OutboxMessageResponse outboxMessage,
            Exception? exception)
        {
            const string sql = @"
            UPDATE outbox_messages
            SET processed_on_utc = @ProcessedOnUtc,
                error = @Error
            WHERE id = @Id";

            return connection.ExecuteAsync(
                sql,
                new
                {
                    outboxMessage.Id,
                    ProcessedOnUtc = _dateTimeProvider.UtcNow,
                    Error = exception?.ToString()
                },
                transaction: transaction);
        }

        internal sealed record OutboxMessageResponse(Guid Id, string Content);
    }
}
